<!doctype html>
<html lang="en-us">
    <head>
        <meta charset="utf-8">
        <title>Keytap2</title>

        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"/>

        <meta name="twitter:card" content="summary">
        <meta name="twitter:title" content="Keytap2" />
        <meta name="twitter:description" content="Acoustic keyboard eavesdropping based on language n-gram frequencies" />
        <meta name="twitter:image" content="https://keytap2.ggerganov.com/keytap2.png" />

        <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
        <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
        <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
        <link rel="manifest" href="/site.webmanifest">

        <meta name="msapplication-TileColor" content="#ffffff">
        <meta name="msapplication-TileImage" content="/ms-icon-144x144.png">
        <meta name="theme-color" content="#ffffff">

        <link rel="stylesheet" href="style.css">
    </head>
    <body>
        <div id="main-controls">
            <div id="description">
                <h2>Keytap2 - acoustic keyboard eavesdropping based on language n-gram frequencies</h2>

                <span class="text-body">
                    This is a proof-of-concept of an attack for recovering the contents of an unknown text just by analyzing the audio from the keyboard strokes.<br>
                    <b>No prior training is required.</b>

                    <br><br>

                    This tool runs entirely in your browser. No data is sent or stored to a server.

                    <br><br>

                    Assumptions and requirements:
                    <ul>
                        <li>The text is in English</li>
                        <li>At least 100 characters of meaningful text are typed</li>
                        <li>Only letters ("a" - "z") and the "Space" keys are pressed (i.e. no Backspace, Shift, numbers, etc.)</li>
                        <li>Noisy mechanical keyboard</li>
                        <li>Microphone in the vicinity of the keyboard</li>
                    </ul>

                    <br>

                    Here is a video demonstrating this type of attack: <a class="nav-link2" href="https://youtu.be/jNtw17S6SR0">Demo (~5 min YouTube video)</a>

                    <br><br>

                    For training-based acoustic eavesdropping, make sure to checkout <a class="nav-link2" href="https://ggerganov.github.io/keytap">Keytap</a>

                    <br><br>

                    To run this program in the browser you will need support for pthreads with WebAssembly (i.e. Chrome or Firefox Nightly).<br>
                    To enable WebAssembly pthreads and SharedArrayBuffer support, check this <a class="nav-link2" href="https://github.com/kripken/emscripten/wiki/Pthreads-with-WebAssembly/">page</a>.<br>
                    <br>
                    Press the "Init" button to start:
                </span>

                <div align="center">
                    <button onClick="doInit()" id="butInit" style="width:60px;height:30px;" disabled>Init</button>
                    <br><br>
                    <div class="nav-link2" id="download-info">Downloading WASM module and n-gram stats. Please wait ...</div>
                </div>
            </div>
            <canvas class="emscripten" id="canvas" oncontextmenu="event.preventDefault()" hidden></canvas>
        </div>

        <div id="footer" class="cell-version">
            <span>
                Build time: <span class="nav-link">@GIT_DATE@</span> |
                Commit hash: <a class="nav-link" href="https://github.com/ggerganov/kbd-audio/commit/@GIT_SHA1@">@GIT_SHA1@</a> |
                Commit subject: <span class="nav-link">@GIT_COMMIT_SUBJECT@</span> |
            </span>
        </div>
        <div id="footer2" class="cell-about">
            <a class="nav-link" href="https://github.com/ggerganov/kbd-audio"><span class="d-none d-sm-inline">View on GitHub </span>
                <svg version="1.1" width="16" height="16" viewBox="0 0 16 16" class="octicon octicon-mark-github" aria-hidden="true"><path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"></path></svg>
            </a>
        </div>

        <script type='text/javascript'>
            window.mobilecheck = function() {
                var check = false;
                (function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) check = true;})(navigator.userAgent||navigator.vendor||window.opera);
                return check;
            };

            var isMobile = mobilecheck();
            var isInitialized = false;

            var screenX = -1;
            var screenY = -1;

            var oldOutputId = 0;

            window.setInterval(function(){
                    if (isMobile) {
                        document.getElementById("footer").innerHTML = '';
                        document.getElementById("footer2").innerHTML = '';
                    }
                    if (isInitialized == false) return;
                    var w = window,
                        d = document,
                        e = d.documentElement,
                        g = d.getElementsByTagName('body')[0],
                        x = w.innerWidth || e.clientWidth || g.clientWidth,
                        y = w.innerHeight|| e.clientHeight|| g.clientHeight;
                    Module._set_window_size(0.99*x, y - 1.40*document.getElementById('footer').clientHeight);
            }, 500);

            function checkLoop() {
                var outputId = Module._get_output_id();
                if (outputId != oldOutputId) {
                    offerFileAsDownload('record.kbd', 'mime/type');
                    oldOutputId = outputId;
                }

                setTimeout(checkLoop, 100);
            }

            function onkeydown(event) {
                if (event.keyCode >= 112 && event.keyCode <= 123) {
                    event.stopImmediatePropagation();
                }
            }

            function init() {
                document.getElementById("butInit").disabled = false;
                document.getElementById("download-info").innerHTML = "WASM module initialized!";

                window.addEventListener('keydown', onkeydown, true);

                document.getElementById("canvas").addEventListener("dragover", (event) => {
                    event.preventDefault();
                });

                document.getElementById("canvas").addEventListener("drop", (event) => {
                    event.preventDefault();

                    dropHandler(event);
                });

                setTimeout(checkLoop, 100);
                //window.requestAnimationFrame(renderFrame);
            }

            function doInit() {
                let constraints = {
                    audio: {
                        sampleRate: 16000,
                        channelCount: 1,
                        echoCancellation: false,
                        autoGainControl: false,
                        noiseSuppression: false
                    }
                };

                let mediaInput = navigator.mediaDevices.getUserMedia( constraints );

                if (isInitialized == false) {
                    Module._do_init();
                    document.getElementById("butInit").disabled = true;
                    document.getElementById("description").hidden = true;
                    isInitialized = true;
                }

                var x = document.getElementById("canvas");
                x.hidden = false;
            }

            //function renderFrame() {
            //    window.requestAnimationFrame(renderFrame);
            //}

            function dropHandler(event) {
                // Prevent default behavior (Prevent file from being opened)
                event.preventDefault();

                // fetch FileList object
                var files = event.target.files || event.dataTransfer.files;

                // process all File objects
                for (var i = 0, f; f = files[i]; i++) {
                    ParseFile(f);
                }
            }

            function ParseFile(file) {
                console.log(
                        "<p>File information: <strong>" + file.name +
                        "</strong> type: <strong>" + file.type +
                        "</strong> size: <strong>" + file.size +
                        "</strong> bytes</p>"
                );

                var reader = new FileReader();
                reader.onload = function(event) {
                    var buf = new Uint8Array(reader.result);
                    var stream = FS.open("record.kbd", "w");
                    FS.write(stream, buf, 0, buf.length, 0);
                    FS.close(stream);

                    Module._do_reload();
                }
                reader.readAsArrayBuffer(file);
            }

            function offerFileAsDownload(filename, mime) {
                mime = mime || "application/octet-stream";

                let content = FS.readFile(filename);
                console.log(`Offering download of "${filename}", with ${content.length} bytes...`);

                var a = document.createElement('a');
                a.download = filename;
                a.href = URL.createObjectURL(new Blob([content], {type: mime}));
                a.style.display = 'none';

                document.body.appendChild(a);
                a.click();
                setTimeout(() => {
                    document.body.removeChild(a);
                    URL.revokeObjectURL(a.href);
                }, 2000);
            }

            var Module = {
                arguments: ["record.kbd", "./data"],
                preRun: [(function() {
                }) ],
                postRun: [(function () {
                    init();
                })
                ],
                canvas: (function() {
                    var canvas = document.getElementById('canvas');
                    //canvas.style.marginLeft = "-230px";
                    canvas.addEventListener("webglcontextlost", function(e) { alert('WebGL context lost. You will need to reload the page.'); e.preventDefault(); }, false);

                    return canvas;
                })(),
                print: (function() {
                    return function(text) {
                        text = Array.prototype.slice.call(arguments).join(' ');
                        console.log(text);
                    };
                })(),
                printErr: function(text) {
                    text = Array.prototype.slice.call(arguments).join(' ');
                    console.error(text);
                },
                setStatus: function(text) {
                    console.log("status: " + text);
                },
                monitorRunDependencies: function(left) {
                }
            };
            window.onerror = function() {
                document.getElementById("download-info").innerHTML = "Failed to initialize the WASM module. Your browser might not be supported.";
                document.getElementById('download-info').style.color='red';
                console.log("onerror: " + event);
            };

        </script>

        <script async type="text/javascript" src="keytap2-gui.js"></script>
    </body>
</html>
